调研

优秀的 PDF 阅读器开源库主要有两个:

  1. pdf.js:是 Firefox 浏览器内置的 PDF 引擎,由 Mozilla 提供支持,目标是创建一个基于 Web 标准的通用 PDF 解析和渲染平台。基于 HTML5,由 JavaScript 编写,可以直接在前端技术中使用
  2. pdfium:是 Chromium 浏览器内置的 PDF 引擎,由 C/C++ 编写,前端使用比较复杂,需要编译为 WebAssembly 使用(未实践,可行性未知)

所以在前端想要做 PDF 阅读器的需求,那 pdf.js 基本可以算是唯一的选择

虽然 pdf.js 很强大、功能很丰富,但是其作为一个底层通用库,对实用功能封装比较少,而且其 API 文档比较简陋,在摸索上手阶段需要耗费不少的功夫,实际使用也需要进行大量开发工作以支持业务

一些 pdf.js 有用的资料

所以在实际项目中,都会对 pdf.js 进行封装或二次开发,当然也有一些封装的比较好的项目:

背景

笔者所在公司现在使用 PDF.js Express,但由于 PDF.js Express 收费昂贵且闭源,现调研转向 ZoteroReader 的可能性和迁移成本

常用功能对比

PDF 阅读

都是基于 pdf.js,差别不大

PDF.js Express

![[Pasted image 20250724103920.png]]

ZoteroReader

![[Pasted image 20250724104000.png]]

PDF 标注

PDF.js Express

提供丰富的标注工具:

![[Pasted image 20250724104039.png]]

ZoteroReader

有限的标注工具,仅有以下工具:

![[Pasted image 20250724104057.png]]

标注的存储与加载

PDF.js Express

const debouncedSave = debounce(() => {
  const annotationManager = webViewerCore?.annotationManager as any
  annotationManager
    .exportAnnotations({
      links: false,
      widgets: false,
    })
    .then((xfdfData: any) => {
      if (!libraryId) return
      ApiSavePDFNote({
        libraryId,
        note: xfdfData,
      }).catch(() => {
        message.error(trans(I18N.libraryV2.saveNoteFailed))
      })
    })
}, 800)

// 监听笔记修改
useEffect(() => {
  if (webViewerCore) {
    const { documentViewer, annotationManager } = webViewerCore
    // 监听文档加载
    documentViewer.addEventListener(
      webViewerCore.DocumentViewer.Events.DOCUMENT_LOADED,
      () => {
        annotationManager.addEventListener('annotationChanged', (annotations, action, { imported }) => {
          if (['delete', 'modify', 'add'].includes(action) && !imported) {
            if (libraryId) {
              debouncedSave()
            } else {
              bookmark(false)
              annotationManager.deleteAnnotations(annotations)
            }
          }
        })
      },
      { once: true },
    )
  }
}, [webViewerCore, libraryId])

// 笔记还原
useEffect(() => {
  if (webViewerCore && note) {
    const { documentViewer, annotationManager } = webViewerCore
    // 监听文档加载
    documentViewer.addEventListener(
      webViewerCore.DocumentViewer.Events.DOCUMENT_LOADED,
      () => {
        annotationManager
          .importAnnotations(note?.trim())
          .then(() => {
            message.success({ content: trans(I18N.libraryV2.loadNoteSuccess) })
          })
          .catch(() => {
            message.error({ content: trans(I18N.libraryV2.loadNoteFailed) })
          })
      },
      { once: true },
    )
  }
}, [webViewerCore, note])

ZoteroReader

源码

const reader = iframeWindow.createReader({
  // 监听笔记修改
  onSaveAnnotations: async function (annotations) {
    console.log('Save annotations', annotations)
  },
  onDeleteAnnotations: function (ids) {
    console.log('Delete annotations', JSON.stringify(ids))
  },
  // 其他配置...
})

// 笔记还原
reader.setAnnotations(annotations)

I18N

PDF.js Express

支持

instance.UI.setLanguage(isChinese ? 'zh_cn' : 'en')

ZoteroReader

支持,不过官网只有英文,需要开发者编写中文文案并加载

![[Pasted image 20250724104203.png]]

编写 ftl 文件(中文文案),进行加载。源码

划词弹窗自定义功能

PDF.js Express

![[Pasted image 20250724104428.png]]

// 划词 popup 添加自定义按钮
instance.UI.textPopup.add([
  // 翻译
  {
    type: 'customElement',
    render: () => <CustomPopupButton buttonType={EPopupButton.TRANSLATE} instance={instance} onButtonClick={onTranslate} />,
  },
  // AI 分析
  {
    type: 'customElement',
    render: () => <CustomPopupButton buttonType={EPopupButton.AIANALYSIS} instance={instance} onButtonClick={onAIAnalysis} />,
  },
])

ZoteroReader

![[Pasted image 20250724104446.png]]

也支持自定义

iframeWindow.addEventListener('customEvent', (event: any) => {
  if (event?.detail?.type !== 'renderTextSelectionPopup') {
    return
  }
  const { append } = event.detail
  append('2342423432')
})

跳转到指定位置并高亮区域

PDF.js Express

![[Pasted image 20250724104555.png]]

instance.Core.documentViewer.addEventListener('annotationsLoaded', () => {
  const { pos, unit } = qs.parse(location.search) as unknown as {
    pos: [number, [number, number, number, number]][]
    unit: string
  }

  if (pos?.[0] && unit === 'percent') {
    const [page, range] = pos[0]
    const newPage = Number(page)
    const newRange = range?.map(item => Number(item))

    const pageInfo = instance.Core.documentViewer.getDocument().getPageInfo(newPage) || {
      width: 0,
      height: 0,
    }

    const x1 = newRange[0] * pageInfo.width
    const y1 = newRange[1] * pageInfo.height
    const x2 = newRange[2] * pageInfo.width
    const y2 = newRange[3] * pageInfo.height

    onNavigate({
      tl: [x1, y1],
      br: [x2, y2],
      page: newPage - 1,
    })
  }

  if (pos?.[0] && unit !== 'percent') {
    const [page, range] = pos[0]
    onNavigate({
      tl: [Number(range[0]), Number(range[1])],
      br: [Number(range[2]), Number(range[3])],
      page: Number(page),
    })
  }
})

ZoteroReader

实际就是 PDF 跳转到指定位置 + 在该区域添加一个标注,ZoteroReader 都支持

其他

PDF 内链接跳转、快捷键(如 ctrl+z)、目录预览、搜索、缩放等其他功能都支持

总结